```latex
\newcommand{\commonfolder}{../../common-files}

\input{\commonfolder/header}
\input{\commonfolder/copyright}
\input{\commonfolder/solidity-highlighting.tex}

\hypersetup{%
    pdfborder = {0 0 0}
}


\lhead{\bfseries SEED Labs -- 智能合约实验室}

\begin{document}

\begin{center}
{\LARGE 智能合约实验室}
\end{center}

\seedlabcopyright{2023}

\newcommand{\contractfolder}{{\path{Labsetup/contract}}\xspace}

% *******************************************
% SECTION
% ******************************************* 
\section{概述}

智能合约是一种在区块链上运行的程序。它的代码是不可变的，并存储在区块链上的特定地址中，当满足预定义条件时会自动执行。智能合约的数据（状态）也存储在区块链上。本次实验旨在帮助学生了解此类程序的工作原理以及如何编写简单的智能合约代码。学生将在SEED区块链模拟器上进行实验，使用提供的智能合约代码。本实验涵盖以下主题：

\begin{itemize}[noitemsep]
    \item 智能合约开发
    \item Remix IDE
    \item 部署和与智能合约交互
    \item 向智能合约发送资金
\end{itemize}

\paragraph{实验环境。} 本文档中的活动已在SEED网站上预构建的Ubuntu 20.04虚拟机中进行测试，请从SEED网站下载。

% *******************************************
% SECTION
% *******************************************
\section{实验设置：启动区块链模拟器}

本次实验将在SEED互联网模拟器（在本文档中简称为“模拟器”）内执行。如果这是您首次使用该模拟器，务必阅读本节内容。我们建议教师提供一个实验室环节来帮助学生熟悉模拟器。

\input{\commonfolder/emulator.tex}


\paragraph{EtherView。}
\input{\commonfolder/etherview.tex}



% *******************************************
% SECTION
% *******************************************
\section{任务1：使用Remix进行智能合约开发}

有许多智能合约开发环境，包括Hardhat、Remix和Truffle。在本实验中，我们将使用Remix IDE（集成开发环境），该IDE可用于编写、编译和调试Solidity代码。它支持测试、调试和部署智能合约。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务1.a：将Remix连接到SEED模拟器}

Remix IDE有两种版本——在线版本和桌面版本，我们将在在线IDE中使用它，因此不需要安装任何内容。只需访问此网址 \url{https://remix.ethereum.org/} 即可获取Remix IDE。Remix在其官方文档 \url{https://remix-ide.readthedocs.io/} 中提供了如何使用的优秀指导。

Remix可以通过几种机制连接到区块链，如Remix VM、注入提供者（MetaMask）和外部HTTP提供者。Remix VM是一种浏览器中的沙盒区块链；它模拟了区块链的行为，因此并非真正的区块链。
我们将通过注入提供者（例如MetaMask）或外部HTTP提供者将Remix连接到我们的SEED区块链模拟器，后者是一个真实的区块链（私有区块链）。


我们将使用MetaMask进行此任务。首先连接MetaMask到SEED区块链模拟器，然后告诉Remix在需要与区块链交互时使用MetaMask。MetaMask不仅充当我们与区块链的桥梁，也是一个钱包：每次我们需要发送交易时，都必须从一个账户进行操作；我们会使用MetaMask来管理这些账户。

在“区块链探索实验室”中，我们已经安装了MetaMask扩展到浏览器上。如果您之前没有执行过该实验，请按照以下链接中的说明进行设置:\url{https://github.com/seed-labs/seed-labs/blob/master/manuals/emulator/metamask.md}。要恢复账户，请使用以下助记短语：

\begin{lstlisting}
gentle always fun glass foster produce north tail security list example gain
\end{lstlisting}

设置好MetaMask后，进入Remix IDE中的“部署 & 运行交易”菜单，在“环境”下拉菜单中设置为“注入提供者 - MetaMask”。如果一切设置正确，则在“账户”下拉菜单中应能看到带有余额的多个账户。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务1.b：编写、编译和部署智能合约}

在此任务中，我们将使用Remix部署一个名为`Hello.sol`的简单智能合约。代码可以在 \contractfolder 文件夹中找到。

\begin{itemize}
    \item 编写代码：转到Remix中的“工作区”菜单，您会看到几个文件夹。
          在`contract`文件夹内创建一个新的文件，命名为 `Hello.sol`。将提供的 `Hello.sol` 文件的内容复制并粘贴到新文件中。

    \item 编译代码：转到“Solidity编译器”菜单，然后编译 `Hello.sol` 程序。

    \item 部署合约：转到“部署 & 运行交易”菜单，并点击“部署”按钮。MetaMask将弹出并要求确认。如果您未看到MetaMask，请检查您的“环境”设置以确保已选择MetaMask。如果部署成功，您应能看到来自MetaMask的确认信息。转至EtherView并显示部署交易已被添加到一个区块。

\end{itemize}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{1.c：幕后操作}

在此任务中，我们将了解当我们部署合约时究竟发生了什么。
部署合约是通过一笔交易完成的，这与资金转账交易不同。请进行一次合约部署，并使用EtherView获取该交易的详细信息。将此交易与其他资金转账交易进行比较，并描述这两种类型的交易之间的主要区别。在合约部署交易中，您会看到数据字段中有某些内容。这些是什么？请提供证据来支持您的结论。

\paragraph{注意：} 在此任务中，可能需要从Remix获取编译合约的字节码。这可以从“Solidity编译器”菜单中获得。您应该能够在那里看到“ABI”和“字节码”按钮。您也可以进入 \contractfolder 文件夹，使用提供的 `Makefile` 编译代码以获取字节码。

% *******************************************
% SECTION
% *******************************************
\section{任务2：调用合约函数}

在此任务中，我们将了解如何与智能合约进行交互。
我们将会使用上一个任务中从`Hello` 合约部署的合约。转到“已部署合约”区域，并应该能看到最近部署的合约及其地址、余额和功能。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务2.a：通过本地调用调用函数}

如果定义为 `public view` 类型的函数不会修改合约的状态，因此可以通过本地调用而不是交易的方式进行调用。这种交互没有成本。Remix 会为此类型的功能创建一个按钮，以及每个 `public` 变量（为每个 `public` 变量都会自动生成一个默认的 `public view` 类型的 getter 函数）。我们可以通过这些按钮与合约进行交互，请调用合约的所有 `public view` 功能，并报告您的观察结果。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务2.b：通过交易调用函数}

如果函数会修改合约的状态，必须通过交易的方式进行调用。Remix 会对这些功能的按钮使用不同的颜色表示。
请调用 `increaseCounter()` 函数，并报告您的观察结果。请使用EtherView展示此调用生成的交易。

添加一个名为 `decreaseCounter(unit)` 的新函数到智能合约中。编译并部署它，与这个新的函数进行交互并显示结果。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务2.c：幕后操作} \label{subsec:under_the_hood}

当我们发送交易以调用一个函数时究竟发生了什么？请查看此类交易的 `to` 和 `data` 字段，并解释它们的意义。要调用一个函数，需要提供函数名和参数。这些信息被编码在 `data` 字段中。
请调用 `increaseCounter()` 函数，在EtherView获取交易详情，然后验证 `data` 字段是否确实按以下方式进行了编码：

\begin{itemize}[noitemsep]
    \item 函数选择器：函数原型的哈希值前4个字节
    \item 参数：根据ABI定义的类型进行编码（每个32字节）
\end{itemize}

以下Python脚本可以帮助您计算一个函数原型的哈希值。

\begin{lstlisting}
from web3 import Web3

hash = Web3.sha3(text="<function signature>")
print(hash.hex())
\end{lstlisting}

理解了实际情况后，我们可以通过数据字段直接调用函数，而不是使用Remix提供的按钮（这些按钮实际上会构造 `data` 字段的内容）。在Remix中，在“已部署合约”区域的底部，您可以看到一个名为 “低级交互”的区域。我们在“CALLDATA”字段中输入的所有内容都将被放置到交易的 `data` 字段中，请使用此方法调用 `increaseCounter()` 函数，并验证调用是否成功。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务2.d：触发事件}

智能合约可以发出事件来通知在区块链上发生了什么。应用程序可以监听这些事件并在它们发生时采取行动。在`Hello.sol`中，我们声明了一个名为 `MyMessage` 的事件，然后在 `sendMessage()` 函数中使用 `emit` 生成一个消息。

\begin{lstlisting}
event MyMessage(address indexed _from, uint256 _value);

function sendMessage() public returns (uint256) {
   emit MyMessage(msg.sender, counter);
   return counter;
}
\end{lstlisting}

生成的消息会被放在交易收据的 `log` 字段内。通过监控此字段，应用程序可以被通知到事件发生。Remix 会显示 `log` 字段，请查看并确认其内容是否符合您的预期。

%\paragraph{TODO:} 
%编写一个Python程序来监视特定类型的事件。每5秒查询节点一次。

% *******************************************
% SECTION
% *******************************************
\section{任务3：向合约发送资金}

在此任务中，我们将向合约发送资金。
为此任务将使用不同的合约。此合约的程序名为 `EtherFaucet.sol`，并可以在 \contractfolder 文件夹中找到。
请复制粘贴Solidity源代码到Remix中，并编译后部署到SEED区块链。

当我们将资金发送给智能合约时，其中一个函数（必须为 `payable` 类型）将会被调用。具体调用哪个函数取决于情况不同。图~\ref{fig:payable_function} 描述了每个情况调用的条件。
在本任务中，我们将会针对每种情况开展实验。

\begin{figure}[htb]
\begin{center}
\includegraphics[width=0.65\textwidth]{Figs/send_fund.pdf}
\end{center}
\caption{当向合约发送资金时会调用哪个 `payable` 函数}
\label{fig:payable_function}
\end{figure}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务3.a：直接向合约地址发送资金} 

可以通过合约的地址直接将资金发送给合约，这通过普通交易来完成。在这种情况下，我们不会在交易中指定要调用哪个函数。
但是当合约收到这些资金时，其中一个函数将会被调用。

请使用MetaMask通过 `EtherFaucet` 合约的地址发送一些资金。一旦交易得到确认，请检查Remix中的智能合约余额。`amount` 变量应该包含与该余额相同的值（单位不同）。执行以下实验：

\begin{itemize}[noitemsep]
    \item 检查 `count` 变量以验证调用的函数是否符合图~\ref{fig:payable_function} 的描述。
    
    \item 尝试从合约中移除 `receive()` 函数，并查看Remix将给出哪些警告信息。
\end{itemize}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务3.b：向可支付函数发送资金}

我们可以发送资金给合约并同时调用一个可支付函数。我们将从Remix发起交易，然后通过 `donateEther()` 函数发送一些以太坊到合约中。通过交易的 `value` 字段指定发送给合约的资金数量。您需要在该字段（位于“部署”按钮上方）中输入一定金额。
调用的函数可以使用 `msg.value` 获取资金金额。
在调用之后，请验证余额、`amount` 变量和 `donationCount` 变量的实际值是否符合预期。

\begin{lstlisting}
function donateEther() external (*@\textbf{payable}@*) {
    amount += (*@\textbf{msg.value}@*);
    donationCount += 1;
}
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务3.c：向非可支付函数发送资金}

能够接收资金的函数必须是 `payable`。请尝试移除 `donateEther()` 函数中的 `payable` 关键字并查看Remix将给出哪些警告信息。
我们还提供了一个名为 `donateEtherWrong()` 的函数，它不是 `payable`。请通过调用此函数向合约发送一些资金。您可能会收到一些警告，但请忽略它们并发送交易。会发生什么？资金转账是否成功？如果失败，发件人是否会丢失资金？

\begin{lstlisting}
function donateEtherWrong() external {
    donationCount += 1;
}
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务3.d：向不存在的函数发送资金}

在此任务中，我们将通过调用一个在合约中不存在的函数来向合约发送资金。然而我们故意犯了一个错误，从而调用了合约中存在的一个函数。
我们来看一下实际上会发生什么。

要调用一个不存在的函数，我们需要直接设置交易的 `data` 字段。正如我们在 \ref{subsec:under_the_hood} 中所做的一样，我们可以使用Remix中的“低级交互”来直接设置 `data` 字段。让我们调用名为 `foo()` 的函数，而该函数在合约中并不存在。
我们将向合约发送一些资金的同时调用此函数，请展示会发生什么。资金转账是否成功？将调用哪个函数？

% *******************************************
% SECTION
% *******************************************
\section{任务4：从合约发送资金}

在上一个任务中，我们看到了如何将资金发送给合约。在此任务中，我们将看到如何从合约向接收者发送资金。
在Solidity中有三种不同的方法可以从中发送以太坊：
`send`、`transfer` 和 `call`。这三种方法都被编译为 CALL 操作码，因此它们具有相同的基本内部流程，但有一些不同之处。
关于应该使用哪种方法的推荐，在互联网上的现有文档已经相当混乱，因为以太坊区块链一直在不断变化。

\paragraph{任务：向外部账户发送资金。} 
我们已经在 `EtherFaucet` 合约中实现了这三种方法来发送资金功能。请使用Remix调用这些方法，并查看是否能够从合约接收以太坊。
需要注意的是，`_amount` 参数使用的单位是wei，因此如果希望获得1个以太，则应将金额设置为 `1e18`。

\begin{lstlisting}
function getEtherViaCall(uint256 _amount) external payable {
    address payable to = payable(msg.sender);
    amount -= _amount;
    (bool success, ) = (*@\textbf{to.call{value: \_amount}("")}@*);
    require(success, "Failure: Ether not sent");
}

function getEtherViaSend(uint256 _amount) external payable {
    address payable to = payable(msg.sender);
    amount -= _amount;
    bool success = (*@\textbf{to.send(\_amount)}@*);
    require(success, "Failure: Ether not sent");
}

function getEtherViaTransfer(uint256 _amount) external payable {
    address payable to = payable(msg.sender);
    amount -= _amount;
    (*@\textbf{to.transfer(\_amount)}@*);
}
\end{lstlisting}

请注意，在此示例代码中，任何人都可以从这个合约获取任意数量的以太坊，因此这绝对不是安全的做法。在真实的智能合约中应该实施访问控制以确保只有授权账户可以从中接收资金。
此外为了简单起见，我们未在代码中加入防止重入攻击的保护措施。
我们有一个专门针对重入攻击的SEED实验。

% *******************************************
% SECTION
% *******************************************
\section{任务5：调用另一个合约}

在一个合约内部，我们可以调用另一个合约的功能。在此任务中，我们将看到此类调用如何工作。
我们提供了一个智能合约 `Caller.sol`，可以从 \contractfolder 文件夹中找到。我们将使用它来调用在任务1中部署的 `Hello.sol` 合约中的函数。

代码片段如下所示。需要从 `Hello` 合约派生一个 `interface`。这样就可以创建一个合约对象，并可以使用接口中的API与 `Hello` 合约进行交互。我们提供了两个调用示例。唯一不同的是一个是只执行调用，另一个在执行过程中向 `Hello` 合约发送资金。

\begin{lstlisting}
function invokeHello(address addr, uint _val) public returns (uint) {
   Hello c = Hello(addr);
   uint256 v =  c.increaseCounter(_val);

   emit ReturnValue(msg.sender, v);
   return v;
}

function invokeHello2(address addr, uint _val) public onlyOwner 
                                               returns (uint) 
{
   Hello c = Hello(addr);
   uint256 v =  c.increaseCounter2{value: 1 ether}(_val);
                                             (*@\pointupleft{发送资金}@*) 
   emit ReturnValue(msg.sender, v);
   return v;
}

interface Hello {
    function sayHello() external pure returns (string memory);
    function getResult(uint a, uint b) external view returns (uint);
    function increaseCounter(uint k) external returns (uint);
    function increaseCounter2(uint k) external payable returns (uint); 
    function getCounter() external view returns (uint);
    function sendMessage() external returns (uint256);
}
\end{lstlisting}

请注意，在此示例代码中，任何人都可以从该合约获取任意数量的以太坊，因此这绝对不是安全的做法。在真实的智能合约中应该实施访问控制以确保只有授权账户可以从中接收资金。
此外为了简单起见，我们未在代码中加入防止重入攻击的保护措施。
我们有一个专门针对重入攻击的SEED实验。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务5.b：向另一个合约发送资金} 

使用 `call()` 方法还可以将资金发送给任务1中部署的 `Hello.sol` 合约。调用 `EtherFaucet.sol` 合约中的 `sendEtherToAddress()` 函数，同时设置 `Hello.sol` 合约地址为 `_to` 参数。

\begin{lstlisting}
function sendEtherToAddress(uint256 _amount, address payable _to) external payable {
    amount -= _amount;
    (bool success, ) = _to.call{value: _amount}("");
    require(success, "Failure: Ether not sent");
}
\end{lstlisting}

% *******************************************
% SECTION
% ******************************************* 
\section{提交}

\seedsubmission

\end{document}
```